home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Aminet 52
/
Aminet 52 (2002)(GTI - Schatztruhe)[!][Dec 2002].iso
/
Aminet
/
docs
/
mags
/
saku15.lha
/
Teksti
/
C-kurssi.txt
< prev
next >
Wrap
Text File
|
1995-11-05
|
36KB
|
1,000 lines
5
1*
{3 C-ohjelmointikurssi - Osa 4
{3 ---------------------------
Ville-Pertti Keinonen
Jälleen kerran kurssin osien välille jäi tarpeettoman pitkä väli. Toivotta-
vasti kurssin tässä vaiheessa kaikki kurssia seuranneet pystyvät jo kuiten-
kin hyödyntämään aiemmissa osissa läpi käytyjä asioita ja ottamaan itse
selvää enemmän standarditoiminnoista, joita ei tässä valitettavasti käydä
kuitenkaan enempää läpi, vaikka näin oli alunperin tarkoitus tehdä. Kuten
kolmannessa osassa mainittiin, useimpien kääntäjien mukana tulee ohjeita
näiden toimintojen käyttöä varten.
{3Tiedon käsittelytekniikkaa
{3--------------------------
Yksi asioista, joita ohjelmoijien tulisi oppia voidakseen hyödyntää tieto-
konettaan tehokkaasti on se, miten ohjelmissa tulisi käsitellä tietoa. Sen
tietäminen, miten muuttujia, tiedostoja ja muistia voidaan käsitellä, ei
sinänsä riitä; on myös osattava ohjelmaa käytännössä kirjoittaessa päättää,
millaisessa muodossa ja missä ohjelman käsittelemät tiedot tulisi
säilyttää.
Kaikki ohjelmat käsittelevät tietoa, riippumatta siitä, mikä niiden varsi-
nainen tarkoitus on. Silloin, kun itse tiedon käsittely ei ole pääasiana,
on tiedon määrä useimmiten suhteellisen pieni, eikä tieto sinänsä ole
oleellista muuta kuin ohjelman ajon aikana. Tällöin tietoa säilytetään
yleensä koneen muistissa. (Useilla ohjelmilla tosin on asetustietoja, joita
tallennetaan massamuistiin.) Useimpien interaktiivisten ohjelmien runko-osa
on tällainen. Jos otamme esimerkiksi terminaaliohjelman, sen täytyy
säilyttää mm. seuraavista asioista tietoja (suuri osa näistä on yleis-
tettävissä muihinkiin ohjelmiin):
- Käyttöliittymä (avoinna olevat ikkunat, niiden sisältö), tarkempi muoto
riippuu yleensä käyttöjärjestelmästä.
- Laitteen ohjaus- ja puskurointitiedot (käyttöjärjestelmästä riippuvia,
kuten edellisessä kohdassa).
- Käyttäjän asetukset (modeemiasetukset, hakemistopolut, "puhelinmuistio"
yms.). Ajon aikana nämä ovat muistissa, muulloin niitä säilytetään kovale-
vyllä.
- Terminaaliemulaation tila (aivan yksinkertaiset asiat, kuten kursorin si-
jainti, nykyinen piirtoväri yms.), mikäli se hoidetaan itse. (Amigassa voi-
daan vaihtoehtoisesti käyttää esimerkiksi laitetta "console.device", jol-
loin itse tarvitsee huolehtia ainoastaan tiedon välityksestä laitteiden
välillä.)
- Ohjelman tila. (Yksinkertaisimmillaan tämä riippuu siitä, missä kohtaa
ohjelmaa prosessorin suoritus kulkee, mutta mikäli ohjelman on tarkoitus
antaa käyttäjän tehdä useita asioita "samanaikaisesti" - mitä kutsutaan
usein virheellisesti "sisäiseksi moniajoksi" - täytyy ohjelman "tietää",
mitä se on tekemässä.)
Näiden tietojen yksinkertaisuuden ansiosta useimmat niistä ovat hyvin hel-
posti säilytettäviä, koska niiden määrä on vakio. Riippuen toteutustavasta,
käytetään lähinnä puhelinnumerolistaa sekä mahdollisesti ohjelman tila- ja
käyttöliittymätietoja varten linkattuja listoja, joista lisää hetken kulut-
tua.
Sellaisissa ohjelmissa tai ohjelman osissa, joiden varsinaisena tarkoituk-
sena on käsitellä suurempia määriä tietoa, voidaan tieto säilyttää kokonaan
muistissa, kokonaan levyllä tiedostossa (ainoastaan välittömästi käsi-
teltävä tiedon yksikkö on muistissa, kuten kurssin edellisen osan esimerk-
kiohjelmassa) tai osittain kummassakin. Yksi oleellisimmista tekijöistä
tässä on ero sen välillä, millaisessa muodossa tieto on levyllä ja muistis-
sa. Kurssin edellisen osan esimerkkiohjelmassa tieto oli täsmälleen samassa
muodossa molemmissa, mikä ei kuitenkaan aina ole hyvä. Yleensä silloin, kun
muistissa voi olla vaihteleva määrä tietoa, tietoon liittyy osoittimia,
joita ei voida tallentaa levylle, koska samat tiedot eivät päädy aina sa-
moihin osoitteisiin. (Poikkeuksena ovat vanhanaikaisemmat järjestelmät,
jotka eivät moniaja ja joissa ohjelmat ja tiedot ladataan yleensä kiintei-
siin osoitteisiin.) Vaihtelevaa tietomäärää säilytetään yleensä linkatuissa
listoissa. Linkattu lista tarkoittaa sitä, että yhdessä tiedon yksikössä on
osoitin seuraavaan (ja mahdollisesti edelliseen, joka mahdollistaa alkion
poistamisen kahden muun alkion välistä käymättä listaa läpi siihen kohtaan
asti).
/*
* Esimerkkiohjelma linkattujen listojen käytöstä ja tiedon
* lataamisesta/tallentamisesta. Ohjelma on hieman rivieditorin
* kaltainen, mutta yksinkertaisempi ja selkeäkäyttöisempi.
* Suuremman rivimäärän käsittelemisessä tällainen on myös hidas.
*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
/*
* Linkatussa listassa käytettävä structure-tyyppi. Koska emme
* löydä alkioita muilla tavoin kuin yksinkertaisen linkin
* kautta, emme tarvitse osoittimia molempiin suuntiin, koska
* poistossa joudutaan käymään listaa läpi jo poistettavan
* alkion löytämistä varten.
*/
struct node {
struct node *next; /* Osoitin seuraavaan alkioon. */
char *string; /* Osoitin merkkijonoon, joka on alkion sisältö. */
};
/*
* Osoittimet listan ensimmäiseen ja viimeiseen alkioon.
* Osoitin ensimmäiseen edustaa periaatteessa koko listaa,
* osoitin viimeiseen ainoastaan nopeuttaa alkioiden
* lisäämistä listan loppuun. Huomaa, että molemmat ovat
* ohjelman ajoa aloitettaessa arvoltaan NULL.
*/
struct node *firstnode, *lastnode;
/* Ohjelman osien prototyypit. */
struct node *getnodenr(int);
struct node *addnode(struct node *, const char *);
void freelist(void);
void listall(void);
void printnode(void);
void removenode(void);
void findtext(void);
void enternew(void);
void savelist(void);
void insertfile(void);
/* Makro, jolla voidaan hypätä yli rivinvaihto stdin:stä. */
#define skiplf() \
do { \
int c; \
if ((c = getc(stdin)) != '\n') \
ungetc(c, stdin); \
} while (0)
int main(int ac, char **av)
{{
/*
* Ohjelmassa on yksinkertaisuuden vuoksi samankaltainen
* tehoton käyttöliittymä kuin edellisen osan
* esimerkkiohjelmassa.
*/
for (;;) {
char cmd[2];
puts("\nValitse toiminto:\n");
puts("l luettele (tulosta) kaikki rivit");
puts("t tulosta rivi");
puts("p poista rivi");
puts("e etsi tekstiä");
puts("k kirjoita uusi rivi");
puts("y tyhjennä rivimuisti");
puts("a tallenna tiedostoon");
puts("i lisää tiedoston sisältö rivimuistiin");
puts("u poistu (ulos) ohjelmasta\n");
scanf("%1s", cmd);
switch (cmd[0]) {
case 'L':
case 'l':
listall();
break;
case 'T':
case 't':
printnode();
break;
case 'P':
case 'p':
removenode();
break;
case 'E':
case 'e':
findtext();
break;
case 'K':
case 'k':
enternew();
break;
case 'Y':
case 'y':
freelist();
puts("\nRivimuisti tyhjennetty");
break;
case 'A':
case 'a':
savelist();
break;
case 'I':
case 'i':
insertfile();
break;
case 'U':
case 'u':
freelist();
return 0;
default:
printf("\nTuntematon toiminto '%c'\n", cmd[0]);
break;
}
}
return 0;
}
/* Alkion etsintä rivinumeron perusteella. */
struct node *getnodenr(int n)
{{
/*
* Muka osoitin edellistä ensimmäiseen alkioon. Koska kenttä
* "next" sijaitsee structuren alussa, se voidaan muka lukea
* tällaisen osoittimen kautta. Rivinumero nolla palauttaa
* tällöin tämän osoittimen, jota voidaan käyttää esimerkiksi
* rivien poistamisen yhteydessä.
*/
struct node *p = (struct node *)&firstnode;
while (n--) {
if (!(p = p->next))
break;
}
return p;
}
/*
* Alkion lisäämistoiminto. Parametreiksi annetaan osoitin
* edelliseen alkioon ja merkkijono, joka tulee uuden alkion
* tietosisällöksi.
*/
struct node *addnode(struct node *prev, const char *string)
{{
struct node *node;
/* Varataan ensin muistista uusi alkio. */
if (node = malloc(sizeof *node)) {
/*
* Varataan muistista kopio merkkijonosta edustamaan
* tietosisältöä.
*/
if (node->string = strdup(string)) {
/*
* Lisätään uusi alkio edellisen perään tai listan
* alkuun, mikäli edellinen on NULL-osoitin.
*/
if (prev) {
node->next = prev->next;
prev->next = node;
} else {
node->next = NULL;
firstnode = node;
}
/*
* Mikäli alkio lisättiin listan viimeisen alkion
* perään, on se nyt viimeinen listassa. Jos lista
* on tyhjä, niin sekä prev että lastnode ovat NULL,
* joten tämä toimii siinäkin tapauksessa.
*/
if (prev == lastnode)
lastnode = node;
/*
* Palataan tässä aliohjelmasta, sillä loppuosa on
* tarkoitettu ajettavaksi siinä tapauksessa, että
* muistin varaaminen epäonnistui. Palautetaan
* osoitin uuteen alkioon.
*/
return node;
}
free(node);
}
printf("Varoitus: Uutta alkiota ei voitu luoda\n");
return NULL;
}
/* Aliohjelma, joka vapauttaa koko listan. */
void freelist(void)
{{
struct node *node, *next;
for (node = firstnode; node; node = next) {
next = node->next;
free(node);
}
firstnode = NULL;
lastnode = NULL;
}
void listall(void)
{{
struct node *node;
int n;
puts("\nMuistissa olevat rivit:\n");
for (n = 1, node = firstnode; node; ++n, node = node->next)
printf("%d: %s\n", n, node->string);
}
void printnode(void)
{{
struct node *node = NULL;
int n;
puts("\nAnna tulostettavan rivin numero");
scanf("%d", &n);
if (n)
node = getnodenr(n);
if (node)
printf("\n%d: %s\n", n, node->string);
else
printf("\nRiviä numero %d ei ole.\n", n);
}
void removenode(void)
{{
struct node *p = NULL, *node;
int n;
puts("\nAnna poistettavan rivin numero");
scanf("%d", &n);
if (n)
p = getnodenr(n - 1);
/*
* p osoittaa edelliseen, joten sillä täytyy olla kenttänä
* myös osoitin seuraavaan.
*/
if (p && (node = p->next)) {
p->next = node->next;
/* Jos node oli viimeinen, se on nyt p. */
if (node == lastnode)
lastnode = p;
free(node->string);
free(node);
} else
printf("\nRiviä numero %d ei ole.\n", n);
}
void findtext(void)
{{
struct node *node;
int n, found = 0;
size_t len, slen;
char buf[256];
char *s;
puts("\nAnna etsittävä merkkijono");
skiplf();
gets(buf);
len = strlen(buf);
if (len && buf[len - 1] == '\n')
--len;
/* Ei etsitä tyhjää merkkijonoa. */
if (!len)
return;
/* Käytetään hyvin yksinkertaista etsintämenetelmää. */
for (n = 1, node = firstnode; node; ++n, node = node->next) {
s = node->string;
slen = strlen(s);
while (*s && slen-- >= len) {
if (!strnicmp(buf, s++, len)) {
++found;
printf("%d: %s\n", n, node->string);
break;
}
}
}
printf("\nMerkkijono löytyi %d riviltä.\n", found);
}
/* Uuden rivin lisäämistoiminto. */
void enternew(void)
{{
char buf[256];
size_t i;
puts("\nKirjoita uusi rivi");
skiplf();
gets(buf);
i = strlen(buf);
if (i && buf[i - 1] == '\n')
--i;
buf[i] = '\0';
addnode(lastnode, buf);
}
/* Rivimuistin tallentamistoiminto. */
void savelist(void)
{{
struct node *node;
char filename[32];
FILE *fp;
int i;
puts("\nAnna tiedoston nimi, johon tallennetaan");
scanf("%31s", filename);
if (fp = fopen(filename, "w")) {
for (i = 0, node = firstnode; node; ++i, node = node->next)
fprintf(fp, "%s\n", node->string);
fclose(fp);
printf("\nTiedostoon kirjoitettiin %d riviä\n", i);
} else
printf("\nTiedostoa \"%s\" ei voitu luoda\n", filename);
}
/* Tiedoston lisääminen. */
void insertfile(void)
{{
char filename[32];
struct node *p;
char buf[256];
FILE *fp;
int n, i;
puts("\nAnna lisättävän tiedoston nimi");
scanf("%31s", filename);
puts("\nAnna rivinumero, jonka perään lisätään");
scanf("%d", &n);
p = getnodenr(n);
if (p) {
if (fp = fopen(filename, "r")) {
for (i = 0; fgets(buf, sizeof buf, fp); ) {
char *s = buf + strlen(buf) - 1;
if (*s == '\n')
*s-- = '\0';
/* Tyhjiä rivejä ei lisätä. */
if (s == buf)
continue;
/* Jos lisääminen epäonnistuu, lopetetaan. */
if (!(p = addnode(p, buf)))
break;
++i;
}
fclose(fp);
printf("Tiedostosta lisättiin %d riviä.\n", i);
}
} else
printf("\nRiviä %d ei ole.\n", n);
}
Ohjelman pitäisi muiden esimerkkiohjelmien tavoin kääntyä melkein millä ta-
hansa kääntäjällä. Käyttö lienee varsin selvää.
Edellinen ohjelma oli esimerkkinä interaktiivisista ohjelmista, joilla
käyttäjä voi käsitellä tietoa. Usein ohjelmat saattavat prosessoida suuren
määrän tietoa säännönmukaisesti, ilman käyttäjän vaikutusta asiaan. Tällai-
nen on esimerkiksi C-kääntäjä. Tämäntyyppisissä ohjelmissa on tiedon käsit-
telyn nopeus erityisen tärkeää ja sen vuoksi käytetään erityisiä tekniikoi-
ta, joilla voidaan nopeuttaa toimintaa yksinkertaisempaan toteutusmene-
telmään verrattuna.
Hyvä esimerkki tällaisesta nopeutustekniikasta on listojen "hashaaminen"
(en tiedä oikeata suomenkielistä termiä) eli se, että lista joistakin
asioista jaetaan useampaan listaan jonkin sellaisen perusteella, joka jakaa
ne melko tasaisesti näihin listoihin ja nopeuttaa etsintää mahdollisimman
paljon. C-kääntäjässä tällaista käytetään mm. symbolien säilyttämiseen.
Listan numero (hash-arvo) voidaan laskea esimerkiksi symbolin nimen merk-
kien ASCII-arvojen summasta. Suoraan ensimmäisen merkin käyttäminen hash-
arvona ei olisi kovin tehokasta, koska arvot eivät olisi tasaisia ja merk-
kijonojen vertailu pääsisi aina hiukan pitemmälle merkkijonossa kuin täysin
erilaisen tapauksessa. Yksinkertaistettuna symbolitoiminnot voisivat olla
esimerkiksi seuraavanlaisia (tässä puuttuvat symbolien varsinaiset tiedot):
/*
* Käytetään kahden potenssia hash-taulukon kokona, jotta
* voitaisiin käyttää nopeaa '&'-toimintoa laskennassa.
*/
#define SYMHASH 256
struct symbol {
struct symbol *next;
/*
* Varaamalla symbolin tunnuksen (nimen) suoraan alkion
* perään säästytään useammalta muistinvarauskutsulta.
*/
char id[1];
};
struct symbol symtab[SYMHASH];
int calchash(const char *s)
{{
int h = 0, c;
while (c = (unsigned char)*s++)
h += c;
return h & (SYMHASH - 1);
}
struct symbol *addsym(const char *id)
{{
struct symbol *sym;
int h = calchash(id);
sym = getmem(sizeof *sym + strlen(id));
strcpy(sym->id, id);
sym->next = symtab[h];
symtab[h] = sym;
return sym;
}
struct symbol *getsym(const char *id)
{{
struct symbol *sym;
int h = calchash(id);
for (sym = symtab[h]; sym; sym = sym->next) {
if (!strcmp(sym->id, id))
break;
}
return sym;
}
/*
* Muistin varaamiseen käytetään normaalia malloc():ia
* nopeampaa ja suurella määrällä varauksia vähemmän
* muistia kuluttavaa toimintoa, joka lisäksi poistuu
* ohjelmasta, mikäli varaus epäonnistuu.
*/
char *memptr;
size_t memleft;
#define CHUNKSIZE 65536
void *getmem(size_t bytes)
{{
void *p;
/* Varmistetaan, että tavumäärä on parillinen. */
bytes = (bytes + 1) & ~1;
if (bytes > CHUNKSIZE)
abort();
/* Jos ei nykyisestä lohkosta ole jäljellä tarpeeksi,
varataan uusi ja unohdetaan nykyinen. */
if (memleft < bytes) {
if (!(memptr = malloc(CHUNKSIZE)))
abort();
memleft = CHUNKSIZE;
}
/* Annetaan muistilohkon seuraavat "bytes" tavua. */
p = memptr;
memptr += bytes;
memleft -= bytes;
return p;
}
Oikeassa ohjelmassa olisi symboleilla varmasti muutakin tietoa, ja getmem()
saattaisi muistaa varaamansa lohkot, jotta ne voitaisiin vapauttaa sitten,
kun symbolitaulukon nykyistä sisältöä ei enää tarvita. C-kääntäjän tapauk-
sessa symbolitaulukkoja olisi lisäksi useampia, koska jokaisen ohjelmaloh-
kon sisällä voi olla paikallisia symboleita. Jokaiselle lohkolle olisi
tällöin myös omat listansa muistilohkoja, joista varattaisiin kaikki sen
lohkon paikalliset tiedot. Käytettyjen tekniikoiden toiminnan pitäisi kui-
tenkin selvitä ylläolevista esimerkkitoiminnoista.
{3Käännöksen vaiheet, C-kääntäjän toiminta
{3----------------------------------------
Kuten aiemmissa osissa on mainittu, C-ohjelma käännetään useassa vaiheessa.
Näiden vaiheiden samoin kuin C-kääntäjän toiminnan tietäminen on hyödyl-
listä. Vaiheet on tässä selostettu lyhyesti sellaisina, kuin ne yleensä
ovat - monet kääntäjät saattavat kuitenkin yhdistää näitä vaiheita tai mah-
dollisesti jakaa niitä edelleen. Sisäisesti kaikki kääntäjät toimivat kui-
tenkin ainakin käännöksen alkuvaiheissa suurin piirtein samalla tavoin.
Ensimmäinen vaihe käännöksessä on esikäsittely, jonka ohjaamiseen käytet-
tyjä komentoja ja sen suorittamia asioita käsiteltiin pääpiirteiltään kurs-
sin toisessa osassa. Esikäsittelijäohjelma on standardilta nimitykseltään
"cpp" (luultavasti tulee sanoista "C preprocessor"), joissakin kääntäjissä
tämä saattaa olla muunnettuna (esimerkiksi DICE:n "cpp" on nimeltään
"dcpp"). Joskus tämä vaihe saatetaan ajaa manuaalisesti (yleensä tosin etu-
liittymän option avulla), jos käsittelyä käytetään vaikka muiden tekstien
kuin C-lähdekoodien käsittelemiseen. (Esimerkiksi "Makefile"-tiedostojen ja
assemblysorsien käsittely on yleistä Unix-systeemeissä.)
Esikäännösvaiheessa C-sorsa käydään nopeasti läpi. Ainoastaan '#'-merkillä
alkavat rivit tulkitaan, kaikki muu ainoastaan selataan läpi siten, että
kommentit poistetaan ja korvataan tyhjällä ja itse koodisisältö käydään si-
ten läpi, että se eritellään yhtenäisiksi sanoiksi, joista symbolin
näköisiä sanoja verrataan makrolistaan ja tarpeen vaatiessa korvataan sym-
bolia vastaavalla makrolla. Lopputuloksena on yleensä alkuperäistä sorsaa
pitempi (include-tiedostojen ansiosta, jotka luetaan suoraan mukaan sorsaan
siihen kohtaan, missä #include-rivi oli) tiedosto, jossa ei ole kommentteja
eikä yleensä muita '#'-rivejä kuin #line-rivejä, jotka kertovat seuraavalle
käännösvaiheelle missä mennään, jotta virheiden kohdat voidaan ilmoittaa
oikein. Vaihdellen kääntäjän mukaan, saatetaan käyttää muitakin '#'-rivejä;
DICE jättää mm. #pragma-komennot ja lisää mahdollisen nopeutetun esikäsit-
telyoption yhteydessä viitteen valmiiksi puolikäännettyyn tiedostoon. Ami-
gan kääntäjissä tämä on yleistä, koska Amigan käyttöjärjestelmän toiminto-
jen hyödyntämistä varten tarvitsee kohtuuttoman suuren määrän include-tie-
dostoja, joiden tulkinta kestää kauan.
Seuraava käännöksen vaihe on yleensä varsinainen kääntäminen, joka koostuu
itsessään useammista vaiheista. Tämän vaiheen hoitaa yleensä "cc1"-niminen
ohjelma (DICE:n vastaava on "dc1"), jota ei juuri missään yhteydessä tar-
vitse ajaa manuaalisesti.
Varsinaisen käännöksen ensimmäinen vaihe koostuu kahdesta "kerroksesta",
leksikaalisesta analyysistä ja koodin parseauksesta. Leksikaalinen analyysi
lukee esikäsiteltyä tiedostoa ja jakaa sitä leksikaalisiin elementteihin.
Näiden elementtien välillä voi olla vapaasti välejä ja tyhjiä rivejä, jotka
eivät itsessään kuulu näihin elementteihin, koska ne eivät tarkoita mitään;
tämän ansiota on C-kielen vapaamuotoisuus. Tämä vaihe käännöksestä saate-
taan usein hoitaa automaattisesti generoidulla "lekserillä". (Näitä voidaan
generoida mm. ohjelmilla lex ja flex.)
Leksikaalisen analyysin elementit annetaan eteenpäin parserille, joka tul-
kitsee niiden merkityksen (lekseri ei yleensä havaitse virheitä ohjelman
muodossa, sen sijaan parseri havaitsee heti, jos elementeistä ei kokonai-
suutena muodostu mitään järkevää) ja rakentaa sen perusteella muistiin sym-
bolitaulukoita, tyyppimääritelmiä ja väliaikaisia rakenteita, joista
myöhemmin generoidaan itse koodi. Koodia voi periaatteessa generoida mel-
kein suoraan parserista, mutta tällöin optimointimahdollisuudet jäävät var-
sin rajalliseksi. Vanha Aztec C vaikutti generoivan koodia melkein tällä
tavoin, nykyisistä kääntäjistä DICE vaikuttaa sen tuottamasta koodista
päätellen miltei näin yksinkertaiselta. Parserit generoidaan usein auto-
maattisesti mm. yacc-, byacc- ja bison-ohjelmilla.
Kääntäjästä riippuen voi vaihdella suuresti, miten monen vaiheen kautta
varsinainen koodi tuotetaan. Optimoivissa kääntäjissä yleensä näitä vaihei-
ta on paljon ja ne kestävät aikansa ja vievät paljon muistia, koska ohjel-
man jokin osa on kokonaisuudessaan muistissa, kun sitä käsitellään. GNU C:n
tapauksessa koodia tuotetaan yksi funktio kerralla, joten itse lähdekoodi-
tiedoston koko ei juurikaan vaikuta muistinkulutukseen.
Ohjelmointikielten kääntämisen tarkemmasta toteutuksesta kiinnostuneiden on
syytä hankkia käsiinsä flex ja bison (molemmat tulevat GNU C:n mukana) ja
lukea näiden ohjeet.
Varsinaisen käännöksen lopputuloksena on yleensä assemblykielinen lähdekoo-
ditiedosto, jotkut kääntäjät tosin saattavat tuottaa suoraan objektikoodia.
Useimpien kääntäjien etuliittymässä on mahdollisuus käännöksen lopettami-
seen tässä vaiheessa, jolloin tuotettua assemblykoodia voi tutkia itse.
Jotkut ohjelmat saattavat myös muutella koodia tässä vaiheessa. (Itse tein
aikoinaan DICE:en ulkoisen optimointilisävaiheen, joka muokkasi koodia
tässä vaiheessa.)
Seuraava vaihe on assemblysorsan kääntäminen objektikoodiksi. Tämä on nopea
vaihe, sillä erityisesti C-kääntäjän tuottama assemblerkoodi on jo hyvin
samankaltaista itse ajettavan koodin kanssa. Eri C-kääntäjien assemb-
lerkääntäjät vaihtelevat hyvin paljon. DICE:n "das" on normaalista
kääntäjästä pelkistetty versio, joka osaa vain tarkoitukseen tarpeelliset
asiat. Sen sijaan GNU C:n assemblerkääntäjä "as" on erittäin monipuolinen,
vaikkakaan se ei muistuta syntaksiltaan Amigan kääntäjiä, sillä se käyttää
mielestäni parempaa MIT-syntaksia Amigalla tavanomaisemman Motorola-syntak-
sin sijaan. Se tukee tosin osittain myös Motorola-syntaksia. Assemb-
lerkäännöksen tuloksena olevien objektikooditiedostojen päätteenä on ".o".
Nämä objektikooditiedostot sisältävät periaatteessa valmista ajettavaa koo-
dia, mutta ne sisältävät symboliviitteitä, joita ei ole vielä paikannettu.
koska kyseiset symbolit sijaitsevat eri objektitiedostoissa tai mahdolli-
sesti linkattavissa funktiokirjastoissa. Objektikooditiedostoja voidaan jo-
ko kerätä yhteen funktiokirjastoiksi tai linkata ajettaviksi ohjelmiksi.
Linkkaamisvaiheessa useampia objektikooditiedostoja sekä funktiokirjastoja
(standardit C-toiminnot sijaitsevat tällaisessa) yhdistetään ajettavaksi
ohjelmaksi. Ero objektikooditiedostojen ja kirjastojen käsittelemisen
välillä on se, että objektikooditiedostot sisällytetään yleensä kaikki oh-
jelmaan, mutta kirjastoista valitaan ainoastaan ne kohdat (entiset objekti-
kooditiedostot), jotka sisältävät sellaisia symboleita, joihin on viitteitä
itse ohjelman objektikooditiedostoissa. Linkkaamiseen käytetty ohjelma on
yleiseltä nimeltään "ld" (DICE:n linkkerin nimi on "dlink").
Normaalisti etuliittymä hoitaa nämä kaikki vaiheet tai valikoidusti vain
tarpeelliset vaiheet. Yleensä etuliittymä tunnistaa sille parametreina an-
nettujen ohjelmien muodot niiden päätteistä ja ajaa ne tarpeellisten vai-
heiden läpi. Etuliittymälle voidaan antaa myös optioita, jotka kertovat
sille, mihin vaiheeseen asti olisi käännettävä. Tyypillisesti suurempien
ohjelmien tapauksessa C-sorsat käännetään ensin objektikoodiksi, ja sitten
annetaan joko etuliittymälle tai suoraan linkkerille kerralla kaikki objek-
tikoodit, jotta niistä linkattaisiin ajettava ohjelma.
{3Suuren projektin kasassapito
{3----------------------------
Edellä kerrottuja tietoja voidaan hyödyntää erityisesti suuria ohjelmia
tehdessä. Tässä on muutamia vihjeitä siitä, mitä kannattaa tehdä jos ohjel-
ma on vähänkin suurempi kuin ei mitään tai koostuu useista ohjelmista.
Aina kannattaa jakaa ohjelmat useisiin lähdekooditiedostoihin. Ohjelman
käyttämät makrot ja tyyppimäärittelyt kannattaa tehdä include-tiedostoihin,
jotka liitetään #include-rivien avulla eri lähdekooditiedostoihin. Lähde-
kooditiedostot kannattaa jakaa loogisesti siten, että tiedoston nimi kuvaa,
minkätyyppisiä funktioita se sisältää. (Lähdekooditiedostojen niminä saat-
taisi olla esimerkiksi "main.c", "init.c", "window.c", "arexx.c" jne.) Ja-
kamalla loogisesti toiminnot tiedostoihin päätyvät globaaliset muuttujat
usein oikeisiin tiedostoihin siten, että jotakin muuttujaa käyttävät funk-
tiot ovat samassa tiedostossa. Sellaiset muuttujat, joita tarvitaan useam-
missa lähdekooditiedostossa, täytyy tietysti muissa kuin määrittelytiedos-
tossa määritellä "extern":ksi. Usein on hyvä ratkaisu laittaa tällaiset ex-
tern-määrittelyt sekä funktioprototyypit johonkin include-tiedostoon.
Ohjelmille kannattaa aina antaa versionumerot. (Näitä ei tarvinne selittää
sen enempää, koska kaikki ovat varmasti nähneet niitä muissa ohjelmissa.)
Versionumero sekä kaikki merkkijonot, jotka sisältävät sen, kannattaa lait-
taa yhteen lähdekooditiedostoon, joka on nopea kääntää mikäli se ei sisällä
muuta. Kehittyneempää ohjelmien versioiden seuraamista varten on olemassa
valmiita ohjelmia, jotka pitävät lukua lähdekoodikohtaisista versioista ja
kaikista koodille tehdyistä muutoksista. Tällainen on esimerkiksi RCS (Re-
vision Control System), josta lötyy Amigalle portattu hieman muuteltu ver-
sio HWGRCS tai jonka voi GNU C:llä kääntää itse. (Alkuperäinen paketti
löytyy useimmista ftp-siteistä /pub/gnu-hakemistosta, nykyinen taitaa olla
rcs-5.7.tar.gz.)
RCS:lle on myös käyttöä helpottava ja muita ominaisuuksia lisäävä CVS-etu-
liittymä (Concurrent Versions System), jonka versiosta 1.3 löytyy Amigalle
portattu versio. Nykyinen versio on tosin jo 1.6, paketin nimi on
cvs-1.6.tar.gz. Molemmista näistä ohjelmista on erityisesti silloin hyötyä,
kun samaa ohjelmaa kehittävät useat ihmiset yhdessä. (Silloin niitä on to-
sin parempi käyttää Unix-systeemeissä, koska Amiga ei tue suoraan useita
käyttäjiä.) Amigalle tehtyihin ohjelmiin kannattaa laittaa myös sellainen
versionumero, jonka Amigan "version"-komento osaa lukea. Riittää, että
laittaa johonkin kohtaan ohjelmaa merkkijonon (jolla itse ohjelman ei tar-
vitse välttämättä tehdä mitään) muodossa "$VER: ohjelma versionumero
(päivämäärä)", jossa päivämäärä on muodossa päivä.kuukausi.vuosi. Jossakin
ohjelmassa voisi olla esimerkiksi:
const char versionstr[] = { "$VER: myprogram 1.2 (10.6.95)" };
Tällaisia versiomerkkijonoja osaa ainakin HWGRCS pitää yllä automaattises-
ti. Niiden automaattista päivitykstä varten voi myös kirjoittaa esimerkiksi
awk-ohjelmia. (gawk, eli GNU awk tulee GNU C:n mukana ja on varsin hyödyl-
linen monessa muussakin - sen käyttö kannattaa opetella erityisesti jos
käyttää (vaikka ei ohjelmoisikaan) Amigan lisäksi Unix-järjestelmiä.)
Yksi hyödyllisimmistä ohjelmista, joita C-kääntäjien kanssa miltei poik-
keuksetta tulee, on make-apuohjelma (DICE:n make on tietysti "dmake"). Sen
käyttö kannattaa ehdottomasti opetella, koska pitemmän päälle ohjelmien
kääntäminen manuaalisesti on todella tuskastuttavaa, jos ohjelmaa tehdessä
ja korjatessa sitä joutuu kääntämään useaan kertaan. (Yleensä ohjelmia jou-
tuu kääntämään hyvin usein.) Kun kirjoittaa ohjelman kääntämistä varten
"Makefile"-tiedoston (tarkempaa tietoa tästä saa kääntäjän make-ohjelman
ohjeista), voi koko ohjelman kääntämisen suorittaa yksinkertaisesti komen-
nolla "make". Lisäetuna make kääntää ainoastaan ne ohjelman osat, jotka
ovat muuttuneet; mikäli objektikooditiedoston päivämäärä on uudempi kuin
lähdekoodin, sitä ei tarvitse kääntää uudelleen. "Makefile"-tiedostoon voi
laittaa myös muita automaattisia toimintoja, kuten komentoja versionumeroi-
den päivittämiseen.
{3C-laajennuksia
{3--------------
Tässä osassa on lueteltuna joitakin C-kielen laajennuksia, joita tietyissä
kääntäjissä on. Näistä löytyy lisää tietoa kääntäjien ohjeista, tässä on
vain mainittuna pari hyödyllistä laajennusta, joita saattaisi olla hyvä
opetella käyttämään.
{3DICE C
DICE:ssä on ylimääräinen automaattinen esikääntäjämakro, __COMMODORE_DA-
TE__, joka korvautuu merkkijonolla, joka on nykyinen päivämäärä yllämaini-
tussa "$VER:"-merkkijonojen vaatimassa muodossa.
DICE:n funktiomäärittelyissä voi määrätä suoraan rekisterit, joita
käytetään parametrien välittämiseen. Rekisteri määritellään muodossa __re-
kisteri, eli esimerkiksi __D0 tai __A0.
DICE:n funktiomäärittelyissä voi käyttää __autoinit- ja __autoexit-
lisämääreitä, joiden avulla funktio saadaan ajettua automaattisesti ohjel-
man käynnistys/poistumisvaiheessa. Näistä on erityisesti hyötyä linkatta-
vien funktiokirjastojen yhteydessä.
{3GNU C
GCC:n switch()-rakenteessa voidaan case-kohtiin antaa jokin lukuväli lait-
tamalla vakioiden väliin "...", esimerkiksi "case 0 ... 9:" toteutuu ar-
voille nollasta yhdeksään.
GCC:ssä on erittäin tehokas (koska se ei rajoita pahasti kääntäjän opti-
mointeja), vaikkakin hieman vaikeakäyttöinen inline-assembly-toiminto,
__asm__(), jolla voidaan laittaa suoraan C-lähdekoodin sekaan assemblyko-
mentoja. Tästä voi olla hyötyä käytettäessä hyvin matalatasoisia operaa-
tioita (kuten pino-osoittimen muuttelua tai MMU:n ohjausta) tai erityisiä
toimintaa nopeuttavia komentoja, joita C-kääntäjä ei osaa suoraan tuottaa
(kuten "bfffo"). Sillä voidaan myös pakottaa muuttujia haluttuihin rekiste-
reihin yms.
{3Bugien etsintä
{3--------------
Mitä suurempi ohjelma, sitä varmempaa on, että siinä on bugeja (ohjelmoin-
tivirheitä). Bugien etsintä ja korjaaminen on erittäin tärkeä osa ohjel-
mointia, joissakin tapauksissa siihen saattaa mennä moninkertaisesti niin
paljon aikaa kuin itse ohjelmakoodin kirjoittamiseen - erityisesti, jos
kirjoittaa ohjelman kerralla ennenkuin kokeilee sitä.
Bugit ilmenevät yleensä kahdella tavalla: ohjelma ei tee mitä sen pitäisi
tai tekee jotain jota sen ei pitäisi tehdä - yleensä kaatuu tai kaataa koko
koneen. Jälkimmäiset bugit ovat yleensä erityisen vakavia ja vaikeita kor-
jata, erityisesti kun Amigan kaltaisessa suojaamattomassa ympäristössä oh-
jelma voi sotkea muita ohjelmia ja sekoittaa siten koko koneen erittäin
helposti. Koneen tahalliseen kaatamiseen riittää vaikka seuraavannäköinen
lause:
*(void **)4 = 0;
Ylläoleva nollaa muistiosoitteen 4, jonka pitäisi osoittaa exec.libraryn
kantaosoitteeseen, joka ei ole nolla. Tietysti tahallinen koneen kaataminen
on aina helppoa - se onnistuu jopa suojatuissa ympäristöissä, ainakin jos
on "root"-käyttäjä: joskus kokeilin huvikseni Linuxissa shell-komentoa "yes
>/dev/kmem", ja kaatuihan se - mutta Amigassa voi sotkea muistia helposti
vahingossakin, jos jokin osoitin ei osoitakaan oikeaan osoitteeseen.
Sellaiset bugit, jotka ilmenevät säännönmukaisesti aina tietyssä tilantees-
sa, ovat yleensä helposti korjattavissa olevia. Tarvitsee ainoastaan etsiä
bugin kohta, jonka voi yleensä päätellä suunnilleen jo siitä, mitä ohjelma
oli sekoamisen hetkellä tekemässä. Tarkemman kohdan voi löytää esimerkiksi
siten, että laittaa ohjelman tulostamaan vähän väliä ilmoituksia siitä,
missä kohtaa ohjelmaa mennään. Myös muuttujien arvon tulostus voi olla
hyödyllistä, sillä niistä usein näkee, mikä on pielessä, erityisesti jos
vika on jossain kohdassa ennen bugin ilmenemiskohtaa. Tämän menetelmän op-
pii parhaiten kokemuksen myötä. Vähitellen bugeja oppii myös "arvaamaan";
parhaimmillaan ohjelman kaatuessa ohjelmoija päättelee lähdekoodia katso-
matta, missä vika on, tai jopa arvaa, missä luultavasti on vikaa ennen kuin
edes kokeilee ohjelmaa.
Vaikka aina voidaan systemaattisesti etsiä bugeja edellämainitulla tavalla,
ei se kuitenkaan yleensä ole helppoa. Joskus bugeja ei tunnu löytävän
millään: voi esimerkiksi nähdä, että muuttujalla on aivan väärä arvo, mutta
ei välttämättä ole kovinkaan selvää, mistä se on peräisin. Bugien etsintää
helpottavat huomattavasti tähän tarkoitukseen suunnitellut apuohjelmat,
joita löytyy runsaasti ilmaisohjelmina. Amigalla yleisimpiä ovat Enforcer,
joka vahtii ohjelman osoittamia muistiosoitteita (se tosin vaatii MMU:n
toimiakseen!) ja Mungwall, joka vahtii muistinvarauksia. Uudempi tulokas,
jota voi käyttää Enforcerin sijasta, vaikka koneessa ei olisi MMU:ta, on
Apurify, josta löytyy versiot DICE:lle ja GNU C:lle (gcc:n nykyisen version
(2.7.0) mukaana tulee Apurify). Näiden ja muiden debuggaustyökalujen käyttö
selviää tietysti ohjeista.
{3Amigan käyttöjärjestelmä
{3------------------------
Tässä kurssissa on käsitelty C-kieltä pääasiassa yleisesti, mutta useimmat
varmasti haluavat myös oppia hyödyntämään Amigan käyttöjärjestelmää. Amigan
käyttöjärjestelmän hyödyntämistä ei tässä varsinaisesti käsitellä, osittain
siksi, etten henkilökohtaisesti pidä siitä, miten C-kieltä on siinä
väärinkäytetty, mutta kerron kuitenkin, mitä sitä varten olisi opeteltava.
Amigan käyttöjärjestelmän toiminnot löytyvät erilaisista kirjastoista (ei
pidä sekoittaa linkattaviin funktiokirjastoihin), jotka sisältävät valtavan
määrän funktioita. Amigan käyttöjärjestelmän hyödyntämiseksi täytyy opetel-
la käyttämään näitä funktioita ja niihin liittyviä structure-tyyppejä.
Näistä löytyy hiukan tietoa Amigan include-tiedostoista ja enemmän ns. "au-
todoc"-tiedostoista, jotka on saatavilla elektronisessa muodossa native de-
veloper kitin mukana. Painettua tietoa löytyy kirjasarjasta Amiga ROM Ker-
nel Reference Manual, jossa on autodocit ja paljon muuta tietoa Amigan oh-
jelmoinnista esimerkkeineen.
Lopuksi liitän mukaan esimerkiksi hieman siistityn (ulkomuodon siistimisen
lisäksi on lisätty mm. onnistumisen tarkistukset ja mahdollisuus keskeyttää
laskenta) version ensimmäisestä varsinaisesta C-ohjelmastani. Se piirtää
Mandelbrotin joukon hyödyntäen Amigan käyttöjärjestelmää. (Tämä on yksin-
kertaisin mahdollinen ohjelma tähän tarkoitukseen, eikä ole erityisen no-
pea, ainakaan ilman FPU:ta.) Ohjelman laskentaosuus pohjautui muistaakseni
vastaavaan BASIC-ohjelmaan. Kannattaa huomata, että ohjelma on vanha, suun-
niteltu toimimaan Amigan käyttöjärjestelmän versiolla 1.3 eikä ole muuten-
kaan kovin joustava.
#include <stdlib.h>
#include <exec/types.h>
#include <intuition/intuition.h>
#include <proto/exec.h>
#include <proto/intuition.h>
#include <proto/graphics.h>
const struct NewScreen newscr = {
0, 0, 320, 256, 5, 0, 1,
CUSTOMSCREEN, NULL, NULL, NULL, NULL
};
struct NewWindow newwin = {
0, 0, 320, 256, 0, 1, IDCMP_VANILLAKEY,
SIMPLE_REFRESH | ACTIVATE | BORDERLESS | RMBTRAP,
NULL, NULL, NULL, NULL, NULL, 0, 0, 320, 256, CUSTOMSCREEN
};
const unsigned short colors[] = {
0x000, 0x058, 0x04a, 0x03c, 0x00f, 0x20f, 0x60e, 0x80d,
0xa0a, 0xf00, 0xf20, 0xf50, 0xf80, 0xfa0, 0xfc0, 0xff0,
0xef0, 0xdf0, 0xcf0, 0xbf0, 0xaf0, 0x9f0, 0x8f0, 0x7f0,
0x6e1, 0x4b3, 0x2a4, 0x095, 0x085, 0x076, 0x066, 0x067
};
int
main(int ac, char **av)
{{
float xp, yp, y, x, X, Y, nx = -3, xx = 1.5, ny = -1.5, dv, xy;
struct RastPort *rp;
struct Screen *scr;
struct Window *win;
long wmask;
int i;
if (scr = OpenScreen(&newscr)) {
LoadRGB4(&scr->ViewPort, colors, 32);
newwin.Screen = scr;
if (win = OpenWindow(&newwin)) {
rp = win->RPort;
wmask = 1 << win->UserPort->mp_SigBit;
xy = ny + (256 * (dv = (xx - nx) / 320));
for (Y = ny; Y <= xy && !(SetSignal(0, 0) & wmask); Y += dv) {
for (X = nx; X <= xx; X += dv) {
x = y = xp = yp = i = 0;
while (xp + yp <= 4 && i < 64) {
y = 2 * x * y + Y;
xp = (x = xp - yp + X) * x;
yp = y * y;
++i;
}
SetAPen(rp, ((i < 64) ? i % 32 : 0));
WritePixel(rp, (long)((X - nx) / dv), (long)((Y - ny) / dv));
}
}
Wait(wmask);
while (GetMsg(win->UserPort))
;
CloseWindow(win);
}
CloseScreen(scr);
}
return 0;
}